﻿using System;
using System.Collections.Generic;
using Test30.Core;
using Test30.Util;

namespace Test30.Engine
{
    public class Dungeon
    {
        public readonly GameEvent<Dungeon, TileEventArgs> TileChanged = new GameEvent<Dungeon, TileEventArgs>();
        public Array2D<Tile> Tiles { get; }
        public ItemCollection Items { get; }
        public EntityCollection Entities { get; }
        public Game Game { get; }
        public bool LightingDirty { get; private set; }
        public bool VisibilityDirty { get; private set; }
        public Rect Bounds => new Rect(Tiles.Size);

        public Dungeon(Game game,int width,int height)
        {
            Game = game;
            Tiles = new Array2D<Tile>(width, height);

            Items = new ItemCollection(this);
            Items.ItemAdded.Add(Items_ItemAdded);
            Items.ItemRemoved.Add(Items_ItemRemoved);

            Entities = new EntityCollection(this);
            Entities.EntityAdded.Add(Entities_EntityAdded);
            Entities.EntityRemoved.Add(Entities_EntityRemoved);
        }
        public Dungeon(Game game) : this(game, 28, 16) { }

        public void Generate(bool isDescending, int depth)
        {
            Items.Clear();
            Entities.Clear();

            Tiles.Fill((pos) => new Tile(TileType.Wall));

            CreateDungeon(isDescending, depth);
        }

        private void CreateDungeon(bool isDescending,int depth)
        {
            IDungeonGenerator generator = new FeatureCreepGenerator();
            object options = new FeatureCreepGeneratorOptions();

            generator.Create(this, isDescending, depth, options);

            // add the hero
            Entities.Add(Game.Hero,depth);

            // place the hero on the stairs
            foreach (Vec pos in Bounds)
            {
                if (Tiles[pos].Type == TileType.StairsDown)
                {
                    Game.Hero.ForcePosition(pos);
                    break;
                }
            }
            ObjectPool.Get?.Spawn(Game.Hero, depth);
        }

        public bool TryFindOpenAdjacent(Vec startPos, out Vec pos)
        {
            List<Vec> positions = new List<Vec>();
            foreach (Direction direction in Direction.Clockwise)
            {
                Vec tryPos = startPos + direction;
                // skip if out of bounds
                if (!Bounds.Contains(tryPos)) continue;

                // skip if not open
                if (!Tiles[tryPos].IsPassable) continue;

                // skip if already an entity there
                if (Entities.GetAt(tryPos) != null) continue;

                // found a possible one
                positions.Add(tryPos);
            }
            // bail if there aren't any
            pos = startPos;
            if (positions.Count == 0) return false;

            // choose one randomly
            pos = Rng.Item(positions);
            return true;
        }
        public bool TryFindOpenTileWithin(Vec startPos, int minRadius, int maxRadius, out Vec pos)
        {
            // find all possible tiles
            List<Vec> positions = new List<Vec>();
            Rect bounds = new Rect(startPos - (Vec.One * maxRadius), Vec.One * (maxRadius + maxRadius + 1));

            foreach (Vec tryPos in bounds)
            {
                // skip if out of bounds
                if (!Bounds.Contains(tryPos)) continue;

                // skip if outside the valid radii
                int distanceSquared = (tryPos - startPos).LengthSquared;
                if ((distanceSquared < minRadius) || (distanceSquared > maxRadius)) continue;

                // skip if not open
                if (!Tiles[tryPos].IsPassable) continue;

                // skip if already an entity there
                if (Entities.GetAt(tryPos) != null) continue;

                // if we got here, we found one
                positions.Add(tryPos);
            }

            // bail if there are none
            pos = startPos;
            if (positions.Count == 0) return false;

            // choose one randomly
            pos = Rng.Item(positions);
            return true;
        }

        public void SetTileExplored(Vec position)
        {
            IDungeonTile tile = Tiles[position];

            if (tile.SetExplored())
            {
                TileChanged.Raise(this, new TileEventArgs((Tile)tile, position));
            }
        }

        public void SetTilePermanentLit(Vec position, bool lit)
        {
            IDungeonTile tile = Tiles[position];

            if (tile.SetTilePermanentLit(lit))
            {
                DirtyLighting();
                TileChanged.Raise(this, new TileEventArgs((Tile)tile, position));
            }
        }

        public void SetTileThingLit(Vec position, bool lit)
        {
            IDungeonTile tile = Tiles[position];

            if (tile.SetTileThingLit(lit))
            {
                TileChanged.Raise(this, new TileEventArgs((Tile)tile, position));
            }
        }

        public void SetTileType(Vec position, TileType type)
        {
            IDungeonTile tile = Tiles[position];

            if (tile.SetTileType(type))
            {
                TileChanged.Raise(this, new TileEventArgs((Tile)tile, position));
            }
        }

        public void SetTileVisible(Vec position, bool isVisible)
        {
            IDungeonTile tile = Tiles[position];

            if (tile.SetIsVisible(isVisible))
            {
                TileChanged.Raise(this, new TileEventArgs((Tile)tile, position));
            }
        }

        public bool HitAt(Vec pos, Action action, Hit hit)
        {
            // hit the dungeon itself
            switch (hit.Attack.Element)
            {
                case Element.Light:
                    SetTilePermanentLit(pos, true);
                    break;

                case Element.Dark:
                    SetTilePermanentLit(pos, false);
                    break;
            }

            // hit the items
            foreach (Item item in Items.GetAllAt(pos))
            {
                item.Hit(action, hit);
            }

            // hit the entity
            bool hitEntity = false;

            Entity entity = Entities.GetAt(pos);
            if ((entity != null) && (entity != action.Entity))
            {
                entity.Hit(action, hit);
                hitEntity = true;
            }

            return hitEntity;
        }
        public void RefreshView(Game game)
        {
            if (VisibilityDirty)
                Fov.RefreshVisibility(game.Hero.Position, this);

            if (LightingDirty)
            {
                Lighting.Refresh(game.Hero.Position, game);
                Fov.RefreshExplored(game.Hero.Position, this);
            }
            VisibilityDirty = false;
            LightingDirty = false;
        }
        public void DirtyLighting()
        {
            LightingDirty = true;
        }

        public void DirtyVisibility()
        {
            VisibilityDirty = true;
            LightingDirty = true;
        }
        public Vec GetOpenItemPosition(Vec startPos)
        {
            Vec currentPos = startPos;

            bool changed = true;
            while (changed)
            {
                int currentCount = Items.CountAt(currentPos);
                changed = false;

                // short-circuit if we hit an empty tile
                if (currentCount == 0) break;

                int found = 0;
                Vec fromPos = currentPos;
                foreach (Direction dir in Direction.Clockwise)
                {
                    Vec pos = fromPos + dir;

                    // make sure it's a valid tile
                    if (Bounds.Contains(pos) && Tiles[pos].IsPassable)
                    {
                        int count = Items.CountAt(pos);

                        // if we found a square just as good, randomly choose between
                        // it and all of the previously found ones in this loop
                        if ((count == currentCount) && (found > 0))
                        {
                            if (Rng.OneIn(found + 1))
                            {
                                // pick this new one
                                currentCount++;
                            }
                            else
                            {
                                // still consider it found even if it was skipped
                                found++;
                            }
                        }

                        if (count < currentCount)
                        {
                            found++;
                            currentPos = pos;
                            currentCount = count;
                            changed = true;
                        }
                    }
                }
            }

            return currentPos;
        }

        #region Event Members

        private void Items_ItemAdded(Item item, EventArgs e)
        {
            DirtyLighting();
        }

        private void Items_ItemRemoved(Item item, EventArgs e)
        {
            DirtyLighting();
        }
        private void Entities_EntityAdded(Entity entity, EventArgs e)
        {
            if (entity.GivesOffLight)
            {
                DirtyLighting();
            }
        }
        private void Entities_EntityRemoved(Entity entity, CollectionItemEventArgs<Entity> e)
        {
            if (entity.GivesOffLight)
            {
                DirtyLighting();
            }
        }
        #endregion

        
    }
}
